using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Text;
using System.Xml;
using System.Data;
using System.Data.SqlServerCe;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.IO;

namespace FeedFly
{
    /// <summary>
    /// Abstract feed class
    /// Overrideen by subclasses for format-specific parsing
    /// GetFeed() has 3 static overloads for obtaining an instantiated subclass (factory pattern).
    /// </summary>
    public abstract class Feed : FeedBase
    {
        protected List<FeedItem> items;
        protected List<String> addressList;
        protected List<String> guidList;

        public List<FeedItem> Items
        {
            get { return this.items; }
        }

        /// <summary>
        /// Abstract feed parsing method
        /// </summary>
        protected abstract void Parse(XmlReader feedReader);

        /// <summary>
        /// Abstract feed item parsing method
        /// </summary>
        protected abstract FeedItem ParseItem(XmlReader feedReader);

        /// <summary>
        /// Abstract feed parsing method to jump to feed items
        /// </summary>
        protected abstract XmlReader AdvanceReaderToItems(XmlReader feedReader);

        protected void AddItem(FeedItem item)
        {
            bool add = this.isNew;

            if (!add)
            {                
                if (this.hasGuids)
                    // see if item has a guid first
                    add = (this.guidList.BinarySearch(item.Guid) < 0);
                else if (this.hasAddresses)
                    // if feed doesn't use guids, try the link address as a guid
                    add = (this.addressList.BinarySearch(item.Link) < 0);
                else
                    // if feed didn't have addresses, it's an empty feed, so allow adding
                    add = true;
            }

            if(add)
                this.items.Add(item);
        }

        private void BuildFeedItemLists()
        {
            // initialize addresses collection
            this.addressList = new List<String>(Settings.MaxItemCount);
            this.guidList = new List<String>(Settings.MaxItemCount);

            // add existing items to list
            SqlCeDataReader rdr = FeedItem.GetItemDataReader(this.id);

            while (rdr.Read())
            {
                this.addressList.Add(rdr.GetString(3));
                if(!rdr.IsDBNull(9))
                    this.guidList.Add(rdr.GetString(9));
            }

            rdr.Close();
            rdr.Dispose();

            // remove excess capacity after adding items
            this.addressList.TrimExcess();
            this.guidList.TrimExcess();

            if (this.addressList.Count > 0)
            {
                this.hasAddresses = true;

                // address list must be sorted before BinarySearch() can be used
                if (this.addressList.Count > 1) this.addressList.Sort();
            }

            if (this.guidList.Count > 0)
            {
                this.hasGuids = true;

                // address list must be sorted before BinarySearch() can be used
                if (this.guidList.Count > 1) this.guidList.Sort();
            }
        }

        /// <summary>
        /// Saves object to the database
        /// </summary>
        public void Persist()
        {
            short unreadDelta = 0;
            int itemCount = this.items.Count;

            // Enforce Item Limit
            if (itemCount > this.itemLimit) itemCount = this.itemLimit;

            SqlCeConnection conn = Data.GetConnection();
            SqlCeTransaction trans = conn.BeginTransaction();
            SqlCeCommand cmd = new SqlCeCommand();

            cmd.Connection = conn;
            cmd.Transaction = trans;

            if (this.id == 0)
            {
                // if id doesn't exist, feed record needs to be created
                cmd.CommandText = "INSERT Feed(FolderId,Title,Address,Link,DownloadedDate,ItemLimit,Format,UnreadCount) VALUES (@FolderId,@Title,@Address,@Link,@DownloadedDate,@ItemLimit,@Format,@UnreadCount)";
                cmd.Parameters.Add(new SqlCeParameter("@FolderId", folderId));
                cmd.Parameters.Add(new SqlCeParameter("@Title", this.title));
                cmd.Parameters.Add(new SqlCeParameter("@Address", this.address));
                cmd.Parameters.Add(new SqlCeParameter("@Link", this.link));
                cmd.Parameters.Add(new SqlCeParameter("@DownloadedDate", this.downloadedDate));
                cmd.Parameters.Add(new SqlCeParameter("@ItemLimit", this.itemLimit));
                cmd.Parameters.Add(new SqlCeParameter("@Format", (byte)this.format));
                cmd.Parameters.Add(new SqlCeParameter("@UnreadCount", itemCount));

                cmd.ExecuteNonQuery();

                cmd.CommandText = "SELECT @@IDENTITY";
                cmd.Parameters.Clear();

                this.id = (int)(decimal)cmd.ExecuteScalar();
            }
            else
            {
                // update downloaded date
                cmd.CommandText = "UPDATE Feed SET DownloadedDate = @DownloadedDate WHERE Id = @FeedId";
                cmd.Parameters.Add(new SqlCeParameter("@DownloadedDate", this.downloadedDate));
                cmd.Parameters.Add(new SqlCeParameter("@FeedId", this.id));

                cmd.ExecuteNonQuery();

                // ****** Enforcing Item Limit  ******                
                cmd.CommandText = "SELECT COUNT(*) FROM FeedItem WHERE FeedId = @FeedId";
                // remove DownloadedDate parameter but keep FeedId parameter
                cmd.Parameters.RemoveAt(0);

                // get count of items in database
                int dbItemCount = (int)cmd.ExecuteScalar();

                // calculate new item total
                int combinedItemCount = dbItemCount + itemCount;
                
                if (combinedItemCount > this.itemLimit)
                {
                    int purgeCount = combinedItemCount - this.itemLimit;

                    string unreadQueryPrefix = "SELECT COUNT(Id)", unreadQuerySuffix = " AND isRead = 0";

                    StringBuilder purgeQueryBuilder = new StringBuilder(unreadQueryPrefix, 175);
                    purgeQueryBuilder.Append(" FROM FeedItem WHERE Id IN(SELECT TOP (").Append(purgeCount);
                    purgeQueryBuilder.Append(") Id FROM FeedItem WHERE FeedId = @FeedId ORDER BY PubDate)");
                    purgeQueryBuilder.Append(unreadQuerySuffix);

                    // find out how many items are read from the collection that will be deleted first
                    cmd.CommandText = purgeQueryBuilder.ToString();
                    int unreadPurgeCount = Convert.ToInt32(cmd.ExecuteScalar());

                    // deleting unread items shouldn't count towards the unread delta
                    unreadDelta = (short)(itemCount - unreadPurgeCount);

                    purgeQueryBuilder.Remove(0, unreadQueryPrefix.Length);
                    purgeQueryBuilder.Insert(0, "DELETE");
                    purgeQueryBuilder.Length -= unreadQuerySuffix.Length;

                    // purge oldest items from this feed
                    cmd.CommandText = purgeQueryBuilder.ToString();

                    cmd.ExecuteNonQuery();
                }
                else
                    // set unread delta
                    unreadDelta = (short)itemCount;

            }

            if (itemCount > 0)
            {
                // prepare command object for inserting items
                cmd.CommandText = "INSERT FeedItem(FeedId,Title,Link,Description,DescriptionEx,PubDate,Author,isRead,Guid) VALUES (@FeedId,@Title,@Link,@Description,@DescriptionEx,@PubDate,@Author,0,@Guid)";
                cmd.Parameters.Clear();
                cmd.Parameters.Add(new SqlCeParameter("@FeedId", this.id));
                cmd.Parameters.Add(new SqlCeParameter("@Title",System.Data.SqlDbType.NVarChar));//1
                cmd.Parameters.Add(new SqlCeParameter("@Link", System.Data.SqlDbType.NVarChar));//2
                cmd.Parameters.Add(new SqlCeParameter("@Description", System.Data.SqlDbType.NVarChar));//3
                cmd.Parameters.Add(new SqlCeParameter("@DescriptionEx", System.Data.SqlDbType.NText));//4
                cmd.Parameters.Add(new SqlCeParameter("@PubDate", System.Data.SqlDbType.DateTime));//5
                cmd.Parameters.Add(new SqlCeParameter("@Author", System.Data.SqlDbType.NVarChar));//6
                cmd.Parameters.Add(new SqlCeParameter("@Guid", System.Data.SqlDbType.NVarChar));//7

                FeedItem item = null;

                // insert items for feed
                for (int i = 0; i < itemCount; i++)
                {
                    item = this.items[i];

                    // some feeds have line feed characters that affect the listview control's output
                    // remove these before saving to database
                    cmd.Parameters[1].Value = item.Title.Replace("\n", " ");
                    cmd.Parameters[2].Value = item.Link;

                    // make sure description is not too big for database field
                    // store excess in description text field
                    if (item.Description.Length > 3000)
                    {
                        cmd.Parameters[3].Value = item.Description.Substring(0, 3000);
                        cmd.Parameters[4].Value = item.Description.Substring(3000);
                    }
                    else
                    {
                        cmd.Parameters[3].Value = item.Description;
                        cmd.Parameters[4].Value = DBNull.Value;
                    }

                    cmd.Parameters[5].Value = item.PubDate;

                    if (item.Author != null && item.Author.Length > 0)
                        cmd.Parameters[6].Value = item.Author;
                    else
                        cmd.Parameters[6].Value = DBNull.Value;

                    if (item.Guid != null && item.Guid.Length > 0)
                        cmd.Parameters[7].Value = item.Guid;
                    else
                        cmd.Parameters[7].Value = DBNull.Value;

                    cmd.ExecuteNonQuery();
                }

                // make sure that repeated calls will not generate duplicate records
                this.items.Clear();

            }

            // adjust unread count for folder
            if(unreadDelta > 0)
                Feed.AdjustUnreadCount(this.id, this.folderId, unreadDelta, conn, trans);
            
            // set persisted flag
            this.persisted = true;

            trans.Commit();
            conn.Close();
            cmd.Dispose();
            conn.Dispose();
        }

        /// <summary>
        /// Downloads latest feed items for feed
        /// </summary>
        public void DownloadUpdate()
        {
            XmlReader feedReader = null;           

            Net.DownloadResponse response = Net.Download(this.address);

            if (CheckHTTPResponse(response))
            {
                feedReader = CreateXMLReader(response.HTTPResponse.GetResponseStream());

                //if(response.HTTPResponse.ContentType.IndexOf("xml") > -1)

                if (feedReader != null)
                {
                    try
                    {
                        // initialize items collection
                        this.items = new List<FeedItem>(10);

                        this.BuildFeedItemLists();

                        this.Parse(feedReader);

                        this.downloadedDate = DateTime.Now;
                    }
                    catch (NotSupportedException nse)
                    {
                        // this exception occurs when XML is not found from web address
                        StringBuilder error = new StringBuilder("A valid feed was not found at address<br />", 400);
                        error.Append(address).Append("<br /><br />The content type was ");
                        error.Append(response.HTTPResponse.ContentType);
                        Screens.Main.QueueNotification(new NotificationMessage(NotificationMessageType.Error, error.ToString()));
                        
                        this.downloadCancelled = true;
                    }
                    catch
                    {
                        // this will catch errors that occur if network connection is lost during parsing
                        this.downloadAborted = true;
                    }
                    finally
                    {
                        feedReader.Close();
                    }
                }

                response.HTTPResponse.Close();
            }
            else
                this.downloadCancelled = true;
        }

        /// <summary>
        /// Updates feed title, address, item limit and folder
        /// </summary>
        public void Update()
        {
            SqlCeConnection conn = Data.GetConnection();
            SqlCeCommand cmd = new SqlCeCommand(
                "UPDATE Feed SET FolderId = @FolderId, Title = @Title, Address = @Address, ItemLimit = @ItemLimit WHERE Id = @FeedId"
                ,conn);

            cmd.Parameters.Add(new SqlCeParameter("@FolderId", folderId));
            cmd.Parameters.Add(new SqlCeParameter("@Title", this.title));
            cmd.Parameters.Add(new SqlCeParameter("@Address", this.address));
            cmd.Parameters.Add(new SqlCeParameter("@ItemLimit", this.itemLimit));
            cmd.Parameters.Add(new SqlCeParameter("@FeedId", this.id));

            cmd.ExecuteNonQuery();

            cmd.Dispose();
            conn.Close();
            conn.Dispose();
        }

        /// <summary>
        /// Constructor for new feeds
        /// </summary>
        protected Feed(int folderId, string address, short itemLimit, XmlReader feedReader)
        {
            this.address = address;
            this.itemLimit = itemLimit;
            this.folderId = folderId;
            this.isNew = true;
            this.items = new List<FeedItem>(10);

            try
            {
                this.Parse(feedReader);
            }
            catch
            {
                this.downloadAborted = true;
            }

            this.downloadedDate = DateTime.Now;
        }

        /// <summary>
        /// Constructor for existing feeds
        /// </summary>
        protected Feed(FeedInterface feed)
            : base(feed)
        { }

        /// <summary>
        /// Feed Factory Method for existing feeds
        /// </summary>
        public static Feed GetFeed(int id)
        {
            FeedInterface feedData = null;
            Feed feed = null;

            // since feed id is already known, get feed details from database
            SqlCeDataReader rdr = Data.ExecuteReader("Feed","PK_Feed",id);

            bool hasRows = rdr.Read();

            if (hasRows)
            {
                feedData = new FeedBase();

                feedData.Id = id;
                feedData.FolderId = rdr.GetInt32(1);
                feedData.Title = rdr.GetString(2);
                feedData.Address = rdr.GetString(3);
                feedData.Link = rdr.GetString(4);
                feedData.DownloadedDate = rdr.GetDateTime(5);
                feedData.ItemLimit = rdr.GetInt16(6);
                feedData.Format = (FeedFormat)rdr.GetByte(7);
            }

            rdr.Close();
            rdr.Dispose();

            if (hasRows)
            {
                switch (feedData.Format)
                {
                    case FeedFormat.Atom:
                        feed = new FeedAtom(feedData);
                        break;
                    case FeedFormat.RSS:
                        feed = new FeedRSS(feedData);
                        break;
                }
            }

            return feed;
        }

        /// <summary>
        /// Feed Factory Method for downloading new feeds
        /// </summary>
        public static Feed GetFeed(int folderId, string address, short itemLimit)
        {
            Feed feed = null;

            Net.DownloadResponse response = Net.Download(address);

            if (CheckHTTPResponse(response))
            {
                feed = GetFeed(folderId, address, itemLimit, response.HTTPResponse.GetResponseStream());
                response.HTTPResponse.Close();
            }

            return feed;
        }

        private static bool CheckHTTPResponse(Net.DownloadResponse r)
        {
            bool result = false;

            if (r.HTTPResponse != null)
            {          
                if (r.HTTPResponse.StatusCode != System.Net.HttpStatusCode.OK)
                {
                    StringBuilder errorMessage = new StringBuilder("Error downloading feed from<br />", 300);
                    errorMessage.Append(r.Address).Append("<br /><br />The server responsed with status ");
                    errorMessage.Append(r.HTTPResponse.StatusCode.ToString());

                    if (r.HTTPResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
                        errorMessage.Append(": Could not find resource.");
                    else if (r.HTTPResponse.StatusCode == System.Net.HttpStatusCode.InternalServerError)
                        errorMessage.Append(": Server Error.");
                    else
                        errorMessage.Append(".");

                    errorMessage.Append(" Please check the feed address.");

                    Screens.Main.QueueNotification(new NotificationMessage(NotificationMessageType.Error, errorMessage.ToString()));
                }
                else
                {
                    result = true;
                }
            }

            return result;
        }

        public static SqlCeDataReader GetFeedDataReader(int folderId, SqlCeConnection conn)
        {
            return Data.ExecuteReader("Feed", "Feed_ByTitleInFolder", new object[] {folderId}, conn);
        }

        public static SqlCeDataReader GetFeedDataReader(int folderId)
        {
            return Feed.GetFeedDataReader(folderId, Data.GetConnection());
        }

        /// <summary>
        /// This method should be used for databinding to list controls such as the datagrid
        /// </summary>
        /// <returns>SqlCeResultSet</returns>
        public static SqlCeResultSet GetFeedResultSet(int folderId, SqlCeConnection conn)
        {
            StringBuilder sql = new StringBuilder(200);
            sql.Append("SELECT id, folderId, title, CASE WHEN unreadcount > 0 THEN title + ' (' + CONVERT(nvarchar(5),unreadcount) + ')' ELSE title END AS formattedTitle FROM Feed WHERE FolderId = ");
            return Data.ExecuteResultSet(sql.Append(folderId).ToString(), conn);
        }

        private static XmlReader CreateXMLReader(Stream xmlStream)
        {
            XmlReaderSettings settings = new XmlReaderSettings();

            settings.IgnoreProcessingInstructions = true;
            settings.IgnoreComments = true;
            settings.IgnoreWhitespace = true;
            
           return XmlReader.Create(new StreamReader(xmlStream), settings);
        }

        /// <summary>
        /// Feed Factory Method for new feeds
        /// </summary>
        public static Feed GetFeed(int folderId, string address, short itemLimit, Stream xmlStream)
        {
            Feed feed = null;
            bool reading = true;
            FeedFormat format = FeedFormat.Unknown;
            
            XmlReader feedReader = CreateXMLReader(xmlStream);

            // start parsing feed
            while (!feedReader.EOF && reading)
            {
                try
                {
                    feedReader.Read();
                }
                catch (NotSupportedException nse)
                {
                    // this exception occurs when XML is not found from web address
                    StringBuilder error = new StringBuilder("A valid feed was not found at<br />", 400);
                    error.Append(address).Append("<br /><br />Check the feed address.");
                    Screens.Main.QueueNotification(new NotificationMessage(NotificationMessageType.Error, error.ToString()));
                    break;
                }
                catch
                {
                    // catch all other exceptions just in case a network connection is dropped during parsing
                    break;
                }

                if (feedReader.NodeType == XmlNodeType.Element)
                {
                    switch (feedReader.Name)
                    {
                        case "rss":
                            reading = false;
                            format = FeedFormat.RSS;
                            break;
                        case "feed":
                            reading = false;
                            format = FeedFormat.Atom;
                            break;
                        case "html":
                            reading = false;
                            format = FeedFormat.HTML;
                            break;
                        default:
                            break;
                    }
                }
            }

            switch (format)
            {
                case FeedFormat.RSS:
                    feed = new FeedRSS(folderId, address, itemLimit, feedReader);
                    break;
                case FeedFormat.Atom:
                    feed = new FeedAtom(folderId, address, itemLimit, feedReader);                    
                    break;
                case FeedFormat.Unknown:
                    feed = new FeedUnknown(new FeedBase());
                    feed.DownloadCancelled = true;
                    break;
                default:
                    break;
            }

            return feed;
        }

        public static void Delete(int feedId, int folderId, bool updateUI)
        {
            SqlCeConnection conn = Data.GetConnection();
            SqlCeTransaction trans = conn.BeginTransaction();

            // first, mark as read so unread count is updated
            Mark(new MarkRequest(feedId, folderId, true), conn, trans);

            SqlCeCommand cmd = new SqlCeCommand("DELETE FROM FeedItem WHERE FeedId = @FeedId", conn, trans);
            cmd.Parameters.Add(new SqlCeParameter("@FeedId", SqlDbType.Int));
            cmd.Parameters[0].Value = feedId;
        
            cmd.ExecuteNonQuery();

            cmd.CommandText = "DELETE FROM Feed WHERE Id = @FeedId";
            cmd.ExecuteNonQuery();

            trans.Commit();
            conn.Close();

            if (updateUI)
                Screens.Main.RefreshFolder(folderId);
        }

        public static void AdjustUnreadCount(int feedId, int folderId, short count, SqlCeConnection conn, SqlCeTransaction trans)
        {            
            // send update to UI
            Screens.Main.AdjustUnreadCount(new UnreadCountAdjustment(folderId, feedId, (int)count, false));

            SqlCeCommand cmd = new SqlCeCommand("UPDATE Feed SET UnreadCount = UnreadCount + @UnreadOffset WHERE Id = @FeedId", conn, trans);
            cmd.Parameters.Add(new SqlCeParameter("@UnreadOffset", SqlDbType.SmallInt));
            cmd.Parameters.Add(new SqlCeParameter("@FeedId", SqlDbType.Int));

            // set properties
            cmd.Parameters[0].Value = count;
            cmd.Parameters[1].Value = feedId;

            cmd.ExecuteNonQuery();

            // update unread count for parent folder
            Folder.AdjustUnreadCount(folderId, count, conn, trans);
        }

        public struct MarkRequest
        {
            public int FeedId;
            public bool IsRead;
            public int FolderId;

            public MarkRequest(int feedId, int folderId, bool isRead)
            {
                FolderId = folderId;
                FeedId = feedId;
                IsRead = isRead;
            }
        }

        public static void Mark(MarkRequest mr, SqlCeConnection conn, SqlCeTransaction trans)
        {
            int feedId = mr.FeedId;
            bool isRead = mr.IsRead;

            SqlCeCommand cmd = new SqlCeCommand(
                "UPDATE FeedItem SET isRead=@isRead WHERE isRead=@isReadPrevious AND FeedId=@FeedId"
                , conn, trans);

            cmd.Parameters.Add(new SqlCeParameter("@isRead", isRead));
            cmd.Parameters.Add(new SqlCeParameter("@isReadPrevious", !isRead));
            cmd.Parameters.Add(new SqlCeParameter("@FeedId", feedId));

            int readCount = cmd.ExecuteNonQuery();

            if (readCount > 0)
            {
                if (isRead) readCount *= -1;

                // check to see if this feed is selected in the item list
                if (Screens.ItemList.GetCurrentFeedId() == feedId)
                    Screens.ItemList.MarkAllItems(isRead);

                // use readcount to update unread count for feed
                Feed.AdjustUnreadCount(feedId, mr.FolderId, (short)readCount, conn, trans);
            }

            cmd.Dispose();
        }

        public static void Mark(MarkRequest mr)
        {
            SqlCeConnection conn = Data.GetConnection();
            SqlCeTransaction trans = conn.BeginTransaction();

            Mark(mr, conn, trans);

            trans.Commit();
            trans.Dispose();
            conn.Close();
            conn.Dispose();
        }

        private static Regex timeZoneRegex = new Regex("UT|EST|EDT|CST|CDT|MST|MDT|PST|PDT", RegexOptions.Compiled);
        private static Match timeZoneMatch;
        private const string MILLISECONDS = ".00";

        /// <summary>
        /// Parses a DateTime from a string retrieved from a feed
        /// </summary>
        protected static DateTime ParseDate(string date)
        {
            // if only the .NET Compact Framework supported DateTime.TryParse()!
            // because it doesn't, try catch must be used
            // If a regular expression could be created for every known
            //   date string for feeds, try catch could be reserved
            //   for when it is really necessary

            // does this date use time zone names from RFC822?
            timeZoneMatch = timeZoneRegex.Match(date, date.Length - 3);

            // if time zone name found, replace with offset numbers
            //   since DateTime cannot parse these
            if (timeZoneMatch.Value.Length > 0)
                date = date.Replace(timeZoneMatch.Value
                    , Data.TimeZones[timeZoneMatch.Value].ToString());

            try
            {
                // try standard date formats first
                return DateTime.Parse(date
                    , CultureInfo.InvariantCulture
                    , DateTimeStyles.AdjustToUniversal);
            }
            catch (FormatException)
            {
                // Universal time should always be stored
                // so it can be displayed properly in any time zone
                return DateTime.UtcNow;
            }
        }


    }
}
